Skip to content

feat(runtime): harden hand-rolled API-key auth (ADR-0036 Phase 1a)#1624

Merged
xuyushun441-sys merged 1 commit into
mainfrom
feat/api-key-auth-impl
Jun 6, 2026
Merged

feat(runtime): harden hand-rolled API-key auth (ADR-0036 Phase 1a)#1624
xuyushun441-sys merged 1 commit into
mainfrom
feat/api-key-auth-impl

Conversation

@xuyushun441-sys
Copy link
Copy Markdown
Contributor

What

Phase 1a of ADR-0036 ("every app is a REST API + MCP server"): harden the hand-rolled API-key auth in the runtime. This is the security foundation that Phase 1b (objectui surfacing) and Phase 2 (MCP transport) build on.

better-auth 1.6.x ships no apiKey plugin, so the runtime owns the full sys_api_key lifecycle. The pre-existing resolveExecutionContext API-key path was insecure/buggy:

  • looked up by the raw key (implying plaintext storage)
  • filtered on a non-existent active field (real field is revoked)
  • no expiry check

Changes

  • New packages/runtime/src/security/api-key.ts — single audited source of truth for key crypto:
    • hashApiKey(raw) — sha256 hex at-rest hash (lookup matches the indexed high-entropy hash exactly → constant-effort comparison).
    • generateApiKey(prefix?) — 256-bit base64url secret; returns { raw, hash, prefix }; raw returned once, never persisted.
    • extractApiKeyX-API-Key or Authorization: ApiKey <token>; Bearer deliberately excluded (sessions own Bearer).
    • parseScopes (JSON-string textarea or array) / isExpired (fail-open only for unparseable values).
  • resolveExecutionContext — hash the inbound key, look it up by the at-rest hash only, reject revoked (where-clause + defensive guard) and expired keys, merge scopes into ctx.permissions. Resolved principals flow through the same role/permission/RLS path as sessions.
  • Export primitives from the security barrel.

Tests (local, all green)

  • api-key.test.ts — crypto/parse/extract (determinism, uniqueness, base64url, Bearer-not-matched, scopes, expiry epoch/ISO/Date).
  • resolve-execution-context.test.ts — verify path: valid (x-api-key + ApiKey) / revoked / expired / unknown / plaintext-stored not matched / scopes / org→tenant / anonymous / Bearer-ignored.
  • Full runtime suite: 360 passed. tsc --noEmit: 0 errors.

Security

Raw keys and hashes never enter logs, responses or error messages. Fail-closed on anything ambiguous. Whitelist matching, not blacklist.

Follow-ups (out of scope, per ADR)

🤖 Generated with Claude Code

better-auth 1.6.x ships no apiKey plugin, so the runtime owns the full
sys_api_key auth lifecycle. The existing resolveExecutionContext API-key
path was insecure/buggy: it looked up by the RAW key (implying plaintext
storage) and filtered on a non-existent `active` field, with no expiry
check.

- Add packages/runtime/src/security/api-key.ts — the single audited source
  of truth for key crypto: hashApiKey (sha256 hex at-rest), generateApiKey
  (256-bit base64url secret, raw returned once, never persisted),
  extractApiKey (X-API-Key / Authorization: ApiKey; Bearer excluded),
  parseScopes (JSON-string textarea or array), isExpired (fail-open only
  for unparseable values).
- resolveExecutionContext: hash the inbound key and look it up by the
  indexed at-rest hash only; reject revoked (where + defensive guard) and
  expired keys; merge JSON-string scopes into ctx.permissions. Resolved
  principals flow through the same role/permission/RLS path as sessions.
- Export the primitives from the security barrel.
- Tests: api-key.test.ts (crypto/parse/extract, 24 cases) and
  resolve-execution-context.test.ts (verify path: valid/revoked/expired/
  unknown/plaintext-not-matched/scopes/org/anonymous/Bearer-ignored).

Security: raw keys and hashes never enter logs, responses or errors;
fail-closed on anything ambiguous; whitelist matching, not blacklist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Building Building Preview, Comment Jun 6, 2026 6:45pm

Request Review

@xuyushun441-sys xuyushun441-sys merged commit 8825f5c into main Jun 6, 2026
7 of 8 checks passed
@github-actions github-actions Bot added the tests label Jun 6, 2026
@xuyushun441-sys xuyushun441-sys deleted the feat/api-key-auth-impl branch June 6, 2026 18:45
@github-actions github-actions Bot added the size/l label Jun 6, 2026
* cannot recover the raw key by probing for partial matches.
*/
export function hashApiKey(raw: string): string {
return createHash('sha256').update(raw, 'utf8').digest('hex');
if (x && x.trim()) return x.trim();
const auth = readHeader(headers, 'authorization');
if (!auth) return undefined;
const m = auth.match(/^ApiKey\s+(.+)$/i);
xuyushun441-sys added a commit that referenced this pull request Jun 6, 2026
…mcp + ADR-0036 amendment (#1627)

Drops the legacy `plugin-` prefix and moves the outbound MCP-server package to
the top level (`packages/mcp`), parallel to `@objectstack/rest` — both are "your
app exposed over a protocol". Inbound MCP stays `@objectstack/connector-mcp`.

- packages/plugins/plugin-mcp-server → packages/mcp; name → @objectstack/mcp;
  internal plugin id → com.objectstack.mcp; build/tsconfig relative paths fixed
  for the new depth. Exported API unchanged (MCPServerPlugin, MCPServerRuntime,
  registerObjectTools, McpDataBridge, …).
- @objectstack/cli: dependency + dynamic-loader pkg id updated.
- Inbound refs (runtime test, spec comment, changeset config, docs) updated.
  Pre-launch clean break — no compat shim (only cli depended on it internally).
- ADR-0036 amendment (2026-06-07): records (A) the rename, (B) granularity =
  per-environment not per-app (one MCP server per env covers all apps; dynamic
  apps via live discovery; key-scope for narrowing), (C) distribution = skills +
  MCP (one generic portable Skill + live MCP, not hand-maintained vendor config
  snippets) — verified Agent Skills is now an open cross-platform standard.
- Status updated: Phase 1a (#1624) + Phase 2 (#1626) shipped; Phase 2b next.

Build + typecheck + tests green (mcp 36, runtime mcp 5); lockfile regenerated.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants